home *** CD-ROM | disk | FTP | other *** search
/ Nebula 2 / Nebula Two.iso / SourceCode / GameKit / Examples / PacMan / PacManView.m < prev    next >
Text File  |  1995-06-12  |  30KB  |  913 lines

  1.  
  2. #import "Text.h"
  3. #import "PacManView.h"
  4. #import "PacManGameBrain.h"
  5. #import "PacManInfoController.h"
  6. #import <libc.h>    // event stuff, misc.
  7. #import <daymisckit/daymisckit.h>
  8. #import "FruitView.h"
  9. #import "Maze.h"
  10. #import "Monster.h"
  11. #import "Player.h"
  12.  
  13. // comment out the next line to disable the cheat modes.  Cheaters can't
  14. // get net high scores, but they can get local high scores.
  15. #define CHEATMODES YES
  16.  
  17. // sound definitions
  18. #define DOTEATSOUND 0        // any normal dot
  19. #define POWERDOTEATSOUND 2    // power dot
  20. #define MONSTEREATSOUND 1    // monsters
  21. #define FRUITEATSOUND 4        // fruit
  22. #define DEADSOUND 3            // player nabbed by a ghost
  23. #define STARTLEVELSOUND 5    // music for start of a game/level
  24.  
  25. #define POINTS (12.0 * scale)    // the size (in points) we draw the text at
  26. #define READY_X ((x - 2 * GHOST_SIZE) * scale + mazePos.x)
  27. #define READY_Y (y * scale + mazePos.y)
  28. #define OFFSET_X (scale * 6)
  29. #define OFFSET_Y (scale * 3)
  30. // decides which screen (1-6) to load for a given level.  Allows us to
  31. // put them in an arbitrary order.
  32. static int screens[NUMSCREENS] = { // which maze image (1-6) to use
  33.                       1, 1, 1, 1, 2, 2, 2, 2,
  34.                       3, 3, 3, 4, 4, 5, 5, 5,
  35.                       1, 2, 0, 0, 3, 4, 0, 5 };
  36.  
  37. static windowX[3] = { 0.0, 362.0, 698.0 };
  38. static windowY[3] = { 0.0, 280.0, 536.0 };
  39.  
  40. @implementation PacManView
  41.  
  42. - initFrame:(const NXRect *)frm    // designated initializer for a view
  43. {    
  44.     [super initFrame:frm];
  45.     backIsColor = NO; // override default; we want image by default
  46.     begin = 128;
  47.  
  48. // initialize game variables
  49.     
  50.     // where the maze is located within our view.
  51.     mazePos.x = 13; mazePos.y = 12;
  52.     eraseReady = NO;
  53.     myorigin.x = 0; myorigin.y = 0;
  54.     scale = 0;
  55.     // a convenient rect; this way I don't have to keep creating a bunch of
  56.     // rects of dimensions GHOST_SIZE by GHOST_SIZE; I re-use this one...
  57.     NXSetRect(&eraseRect, 0, 0, GHOST_SIZE, GHOST_SIZE);
  58.     NXSetRect(&textEraseRect, 0, 0, 3 * GHOST_SIZE, 2 * GHOST_SIZE);
  59.     NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale);
  60.     // make sure power dots get drawn
  61.     erasePwr = YES;
  62.     
  63.     return self;
  64. }
  65.  
  66. - setGhostTracker:(GKTrackerId)tracker { ghostId = tracker; return self; }
  67. - setFruitTracker:(GKTrackerId)tracker { fruitId = tracker; return self; }
  68.  
  69. // Called by appDidInit to load up images, etc. that we need.
  70. - loadPix
  71. {
  72.     int i; id dotSound = [customSound soundNum:DOTEATSOUND type:0];
  73.     const int *gh;
  74.  
  75.     [super loadPix];
  76.     
  77.     // the dots play through teir own private stream with our special params
  78.     [dotSound setPlayStream:[[GKSoundStream alloc] initStreams:1]];
  79.     [dotSound setPercentToPlay:0.01]; // this number works well... :-)
  80.     backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
  81.     fruitPointCount = 0;
  82.     
  83. // build the ghost objects
  84.     gh = [maze ghosts]; 
  85.     for (i=0; i<=3; i++) {
  86.         ghost[i] = [[Monster alloc]
  87.                 initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
  88.         ghostPointCount[i] = 0;
  89.     }
  90.     
  91. // get the images
  92.     fruit[1] = [NXImage findImageNamed:"Fruit.tiff"];
  93.     fruit[2] = [NXImage findImageNamed:"FruitBig.tiff"];
  94.     gameOver[1] = [NXImage findImageNamed:"GameOver.tiff"];
  95.     gameOver[2] = [NXImage findImageNamed:"GameOverBig.tiff"];
  96.     [gameOver[1] getSize:&(gameOverSize[1])];
  97.     [gameOver[2] getSize:&(gameOverSize[2])];
  98.     [self setScale:[preferences scale]];
  99.     return self;
  100. }
  101.  
  102. - ghost:(int)i                // return ghost #i
  103. {
  104.     return ghost[i];
  105. }
  106.  
  107.  
  108. // State machine...called by the timed entry.  This state machine basically
  109. // controls all the aspects of the game; depending upon it's state, different
  110. // things can/will happen.  The best way to figure this one out is to sit
  111. // down and draw a diagram show how the states transition from one to the
  112. // next.  Note that there are several counters, etc. that function as smaller
  113. // independent state machines that operate within the context of this larger
  114. // state machine.  (Dots blinking, monster states, etc. all run independently
  115. // from main state machine, although the main machine occasionally interrupts
  116. // things in the sub-machines.)  I've not yet had time to fully document the
  117. // logic involved in this state machine...if I ever have the time, I may do so,
  118. // but I don't know that anyone would care if I did anyway.  Minor changes in
  119. // here could render the entire game non-functional if you aren't careful!
  120. // UNLESS YOU KNOW WHAT YOU'RE DOING, DON'T MESS WITH THIS CODE!  Take time
  121. // to understand it fully before playing with it!
  122. // This is the most ridiculously long method you'll ever see, but there's
  123. // no really efficient way to break it up.  (Each section deals with what
  124. // happens in a specific state, and breaking into smaller methods isn't worth
  125. // while, since things are repeated...and it's silly to proliferate subroutines
  126. // that only get called once and from one place.)
  127. - autoUpdate:sender
  128. {            // ALL animation is controlled from here!!!
  129.     register int i, lcnt;
  130.     register BOOL dotEat = NO;
  131.     int x, y;
  132.     BOOL flag = NO;
  133.     BOOL updateFlag = NO;
  134.     float gx, gy;
  135.     NXRect tRect;
  136.  
  137.     // keep track of how many time we've been called; we can use it do
  138.     // decide when to do certain things...
  139.     if ([NXApp isHidden]) return self; // don't suck cycles if we're hidden
  140.     cycles++;
  141.     if (((![preferences speed]) || ([preferences speed] == 2))
  142.             && (!demoMode)) { // slow speed, so ignore 50% of entries
  143.         if (cycles & 0x01) return self;
  144.     }
  145.  
  146.     if (!(cycles & 0x0f)) { // power dots always blink no matter what.
  147.         [self blinkPowerDot];    // happens every 16 cycles.
  148.         erasePwr = YES;
  149.     }
  150.     
  151.     if (fruitPointCount) {
  152.         if (!--fruitPointCount) {
  153.             fruitPointCount = WIPETEXT;
  154.     }    }
  155.  
  156.     for (i=0; i<4; i++) {
  157.         if (ghostPointCount[i]) {
  158.             if (!--ghostPointCount[i]) {
  159.                 ghostPointCount[i] = WIPETEXT;
  160.     }    }    }
  161.  
  162.     if (state == GAMEOVER) {
  163.     // when demowait counter hits WAITFORDEMO, we start up demo mode.
  164.     // it basically restarts the game--but with "demoMode" turned on.
  165.         if (demoWait++ == WAITFORDEMO) {
  166.             state = NORMALSTATE;
  167.             [self restartGameForDemo:YES];
  168.             [controller unpause];
  169.             [[self window] setTitle:"PacMan Demo"];
  170.             [scoreKeeper resetScore];
  171.     }    }
  172.     
  173.     // in this state, the "Get Ready!" sign has been put up; we stall for a]
  174.     // while and then we take it away and start up the next level.
  175.     if ((state == READY) || (state == DIEREADY)) {
  176.         for (i=0; i<=3; i++) {
  177.             [ghost[i] move:self];
  178.         }
  179.         if (!(--begin)) {
  180.             eraseReady = YES;
  181.             if (demoMode) [player resetPlayer];
  182.             state = NORMALSTATE;
  183.             updateFlag = YES;
  184.     }    }
  185.  
  186.     // make the maze blink on and off at the end of the level.
  187.     if (state == BLINK_LEVEL) {
  188.         if (begin--) {
  189.             if (begin < 32) {
  190.                 if (!(cycles & 0x03)) { // make the maze blink
  191.                     [maze visible:(![maze isVisible])];
  192.                     updateFlag = YES;
  193.             }    }
  194.         } else {
  195.             state = READY;
  196.             [maze visible:YES];
  197.             [player resetPlayer];
  198.             [controller nextLevel];
  199.             updateFlag = YES;
  200.             begin = 40;
  201.         }
  202.     }
  203.  
  204.     // stall.  when player dies, the player object needs time to get through
  205.     // it's whole sequence.
  206.     if (state == DYING_PAC) {
  207.         if (!(--begin)) {    // stall
  208.             if (![player newPlayer]) {    // no pacs left == game over.
  209.                 [controller gameOver];
  210.                 state = GAMEOVER;
  211.                 updateFlag = YES;
  212.             } else { // do ready, next pac...
  213.                 state = DIEREADY;
  214.                 [self startScreen];
  215.                 [player resetPlayer];
  216.                 updateFlag = YES;
  217.                 begin = 40;
  218.     }    }    }
  219.  
  220.     // this state is the meat of the game.  move the player and monsters
  221.     //  and deal with player/ghost collisions and eating dots and fruit.
  222.     if (state == NORMALSTATE) {
  223.         // move all the ghosts and the pac
  224.         if (!paused) {
  225.             if (demoMode) lcnt = 2;
  226.             else {
  227.                 lcnt = [preferences speed] / 2 + 1;    // allow hyper speeds:
  228.             // since 0 <= speed <= 3, we'll move one or two frames per update
  229.             }
  230.             while (lcnt) {
  231.                 // figure out how to move player
  232.                 [player move:self];
  233.  
  234.                 // put up/take away the fruit
  235.                 [maze playerPosition:&x :&y]; // get position of fruit
  236.                 if ((numFruits < 3)||demoMode) { // only two fruits per level
  237.                     if (fruitCount++ == timeToFruit) { // time for new fruit ?
  238.                         numFruits++;
  239.                         if (numFruits < 3) fruitOn = DRAW;
  240.                         NXSetRect(&tRect, x * scale + mazePos.x,
  241.                                 y * scale + mazePos.y, FRUIT_SIZE * scale,
  242.                                 FRUIT_SIZE * scale);
  243.                         [self rebuildStaticAt:&tRect];
  244.                     } else // one or the other
  245.                     if (fruitCount == ERASEFRUIT) {
  246.                         // it's been there a while... so remove it
  247.                         timeToFruit = 200 + (random() & 0x01f0);
  248.                         fruitOn = ERASE;
  249.                         if (numFruits == 2) numFruits++;
  250.                         NXSetRect(&tRect, x * scale + mazePos.x,
  251.                                 y * scale + mazePos.y, FRUIT_SIZE * scale,
  252.                                 FRUIT_SIZE * scale);
  253.                         [self rebuildStaticAt:&tRect];
  254.                         fruitCount = 0;
  255.                     }
  256.                 }
  257.             
  258.                 // handle eating dots
  259.                 if ([maze eatDotAt:[player xpos] :[player ypos]]) {
  260.                     [customSound playNum:DOTEATSOUND];
  261.                     dotEat = YES;
  262.                 }
  263.                 if ([player pacAlive]) { // if double timing, need to check.
  264.                     if ([maze powerDotAt:[player xpos] :[player ypos]]) {
  265.                         [customSound playNum:POWERDOTEATSOUND];
  266.                         [scoreKeeper resetBonus:ghostId];
  267.                         flag = YES;
  268.                         dotEat = YES;
  269.                 }    }
  270.                 if (dotEat) {
  271.                     [maze lastDot:&x :&y];
  272.                     NX_X(&eraseRect) = x * scale + mazePos.x;
  273.                     NX_Y(&eraseRect) = y * scale + mazePos.y; 
  274.                     [self rebuildStaticAt:&eraseRect];
  275.                     dotEat = NO;
  276.                 }
  277.                 if (![maze dots]) {
  278.                     state = BLINK_LEVEL;
  279.                     begin = 64;
  280.                     fruitOn = ERASE; // erase fruit if finished level
  281.                     fruitPointCount = 0;
  282.                     for (i=0; i<4; i++) ghostPointCount[i] = 0;
  283.                     [maze playerPosition:&x :&y]; // get position of fruit
  284.                     NXSetRect(&tRect, x * scale + mazePos.x,
  285.                             y * scale + mazePos.y, FRUIT_SIZE * scale,
  286.                             FRUIT_SIZE * scale);
  287.                     [self rebuildStaticAt:&tRect];
  288.                     fruitCount = 0;
  289.                 }
  290.  
  291.                 // see if player ate the fruit
  292.                 [maze playerPosition:&x :&y]; // get position of fruit
  293.                 if (fruitOn == YES) { // fruit's there...
  294.                     if ((abs([player ypos] - y) < GHOST_SIZE / 2) &&
  295.                         (abs([player xpos] - x) < GHOST_SIZE / 2)) { // ate it!
  296.                         fruitOn = ERASE;
  297.                         [customSound playNum:FRUITEATSOUND];
  298.                         NXSetRect(&tRect, x * scale + mazePos.x,
  299.                                 y * scale + mazePos.y, FRUIT_SIZE * scale,
  300.                                 FRUIT_SIZE * scale);
  301.                         [self rebuildStaticAt:&tRect];
  302.                         // add value of fruit to the score
  303.                         fruitPoints = [scoreKeeper
  304.                                 addBonusToScore:fruitId advance:NO];
  305.                         // tell the player how many points he/she got
  306.                         ftx = (x + TEXTOFFSET) * scale + mazePos.x;
  307.                         fty = y * scale + mazePos.y;
  308.                         ftx2 = ftx + OFFSET_X; fty2 = fty + OFFSET_Y;
  309.                         
  310.                         // now, align coords to the maze so erase
  311.                         // does it's job right (otherwise, we end up SOVERing
  312.                         // maze parts twice, and it looks _ugly_!
  313.                         ftx -= mazePos.x; fty -= mazePos.y;
  314.                         // chop off lower four bits (floor to mult. of 16)
  315.                         ftx /= 16 * scale;
  316.                         ftx = (ftx << (scale + 3)) + mazePos.x;
  317.                         fty /= 16 * scale;
  318.                         fty = (fty << (scale + 3)) + mazePos.y;
  319.                         ZAPRECT(textEraseRect, ftx, fty);
  320.                         fruitPointCount = 48;
  321.                 }    }
  322.  
  323.                 // check for player/ghost collision: (and deal with it)
  324.                 if ([maze dots]) {
  325.                     for (i=0; i<=3; i++) {
  326.                         // tell ghost of power dot
  327.                         if (flag) [ghost[i] powerDot:YES];
  328.                         [ghost[i] at:&gx :&gy];
  329.                         if ((abs(gx - [player xpos]) < GHOST_SIZE / 2) &&
  330.                             (abs(gy - [player ypos]) < GHOST_SIZE / 2)) {
  331.                         // collision! decide who dies...
  332.                             switch ([ghost[i] munch]) {
  333.                                 case YES : {    // player got ghost
  334.                                     [customSound playNum:MONSTEREATSOUND];
  335.                                     // (-munch already added any bonus.)
  336.                                     gtx[i] = (gx + TEXTOFFSET) * scale
  337.                                             + mazePos.x;
  338.                                     gty[i] = gy * scale + mazePos.y;
  339.                                     gtx2[i] = gtx[i] + OFFSET_X;
  340.                                     gty2[i] = gty[i] + OFFSET_Y;
  341.                                     ghostPoints[i] = [scoreKeeper
  342.                                         addBonusToScore:ghostId
  343.                                         advance:YES];
  344.                                     // now, align to maze as above
  345.                                     gtx[i] -= mazePos.x; gtx[i] /= scale * 16;
  346.                                     gty[i] -= mazePos.y; gty[i] /= scale * 16;
  347.                                     gtx[i] = (gtx[i] << (3 + scale))
  348.                                             + mazePos.x;
  349.                                     gty[i] = (gty[i] << (3 + scale))
  350.                                             + mazePos.y;
  351.                                     ZAPRECT(textEraseRect, gtx[i], gty[i]);
  352.                                     ghostPointCount[i] = 48;
  353.                                     break;
  354.                                 }
  355.                                 case NO : {        // ghost got player, so die...
  356.                                     if (![player pacAlive]) break;
  357.                                     [customSound playNum:DEADSOUND];
  358.                                     if (cheatMode) break;
  359.                                     state = DYING_PAC;
  360.                                     begin = 48; // stall
  361.                                     [player pacDie];
  362.                                     // now, erase the fruit
  363.                                     fruitOn = ERASE;
  364.                                     [maze playerPosition:&x :&y];
  365.                                     NXSetRect(&tRect, x * scale + mazePos.x,
  366.                                             y * scale + mazePos.y,
  367.                                             FRUIT_SIZE * scale,
  368.                                             FRUIT_SIZE * scale);
  369.                                     [self rebuildStaticAt:&tRect];
  370.                                     updateFlag = YES;
  371.                                     break;
  372.                                 }
  373.                                 case HARMLESS : 
  374.                                 default : {        // eyes do nothing.
  375.                                     break;
  376.                 }    }    }    }    }
  377.  
  378.                 // figure out where ghosts will go next
  379.                 for (i=0; i<=3; i++) {
  380.                     [ghost[i] move:self];
  381.                 }
  382.                 if (lcnt > 1) { // make movement take effect w/o render
  383.                 // doing this extra movement gives faster perceived speeds
  384.                     for (i=0; i<=3; i++) {
  385.                         [ghost[i] moveOneFrame];
  386.                         [ghost[i] powerCount];
  387.                     }
  388.                     [player moveOneFrame];
  389.                 }
  390.                 lcnt--;
  391.     }    }    }
  392.     // draw all the changes, if applicable.
  393.     if (updateFlag) [self update];
  394.     /*if ((state != READY) && (state != DIEREADY))*/ [self updateSelf:&bounds :1];
  395.     return self;
  396. }
  397.  
  398. // This renders the whole screen, much like updateSelf:: below, but since
  399. // it always redraws the _entire_ screen, it is unnaceptably inefficient for
  400. // handling individual animation frames.
  401. - drawSelf:(NXRect *)rects :(int)rectCount    // redraws the screen.
  402. {        // right now, it's stupid and always redraws the whole view.
  403.     //register int i;//, f;
  404.     //int x, y;
  405.     NXPoint pos;
  406.     //NXRect from;//, bezel, mazeRect;
  407.  
  408.     if ([self window] == nil)
  409.         return self;    // exit if no window to draw in
  410.     
  411.     if ((state == BLINK_LEVEL) && ![maze isVisible])
  412.         [dirtPile fullRedraw:self :backGround2];
  413.     else [dirtPile fullRedraw:self :staticBuffer];
  414.    
  415.     if (state == GAMEOVER) {
  416.         pos.x =  (NX_WIDTH(&bounds) - gameOverSize[scale].width) / 2;
  417.         pos.y = (NX_HEIGHT(&bounds) - gameOverSize[scale].height) / 2;
  418.         [gameOver[scale] composite:NX_SOVER toPoint:&pos];
  419.     }
  420.     if (NXDrawingStatus == NX_PRINTING) { // make sure actors are in print
  421.         [self updateSelf:rects :rectCount];
  422.     }
  423.     NXPing();
  424.     return self;
  425. }
  426.  
  427. // This is the main drawing here.  It works like drawSelf, but only
  428. // _changes_ stuff; it doesn't re-draw the whole view each time.  This
  429. // has been done to speed things up.  We erase the old, and then redraw
  430. // all the spots we erased after calculating where things have moved to.
  431. - updateSelf:(NXRect *)rects :(int)rectCount    // redraws the screen.
  432. {        // it redraws only what has changed since last redraw.
  433.     register int i;
  434.     int x, y, j, order[4], flag[4];
  435. //    NXRect from;
  436.  
  437.     if ([self window] == nil) return self;    // exit if no window to draw in
  438.     // if blinking maze and maze isn't on screen then this update is unneeded
  439.     // note that technically, even if maze is visible, we could skip out, but
  440.     // we don't because we want the last dot and the monsters, etc. to go
  441.     // away right as we enter BLINK_LEVEL; this won't happen otherwise.
  442.     if (((state == BLINK_LEVEL) && ![maze isVisible]) || (state == GAMEOVER))
  443.         return self;
  444.     [buffer lockFocus];
  445.     // erase ghosts and ghost text fields.
  446.     for (i=0; i<=3; i++) {
  447.         if ((ghostPointCount[i] == WIPETEXT) && (state != GAME_OVER)) {
  448.             ghostPointCount[i] = 0;
  449.             ZAPRECT(textEraseRect, gtx[i], gty[i]);
  450.             CLRRECT(textEraseRect);
  451.         }
  452.         if (state != GAME_OVER) {
  453.             [ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  454.             NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
  455.             NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
  456.             CLRRECT(eraseRect);
  457.         }
  458.     }
  459.     
  460.     // erase ready text
  461.     if (eraseReady) {
  462.             [maze playerPosition:&x :&y];
  463.             eraseReady = NO;
  464.             ZAPRECT(readyRect, READY_X, READY_Y);    // erase ready text
  465.             CLRRECT(readyRect);
  466.     }
  467.     
  468.     // erase fruit text
  469.     if ((fruitPointCount == WIPETEXT) && (state != GAME_OVER)) {
  470.         fruitPointCount = 0;
  471.         ZAPRECT(textEraseRect, ftx, fty);
  472.         CLRRECT(textEraseRect);
  473.     }
  474.  
  475.     // erase the player's PacMan
  476.     if (state != GAME_OVER) {
  477.         [player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  478.         NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
  479.         NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
  480.         CLRRECT(eraseRect);
  481.     }
  482.     
  483.     // put the player's Pac on the screen if in a state where it's visible.
  484.     if ((state != BLINK_LEVEL) || ((state == BLINK_LEVEL) && (begin > 31))) {
  485.         [player renderAt:mazePos.x :mazePos.y
  486.             move:((!paused) && (state == NORMALSTATE))];
  487.         [player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  488.         NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
  489.         NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
  490.         [dirtPile addRegion:&eraseRect];
  491.     }
  492.         
  493.     // put ghosts on screen if we're in a state where they are visible.
  494.     if ((state != DYING_PAC) && (state != BLINK_LEVEL) &&
  495.             (state != GAMEOVER)) {
  496.         for (i=0; i<=3; i++) {
  497.             [ghost[i] renderAt:mazePos.x :mazePos.y move:
  498.                 ((!paused) && (state == NORMALSTATE))];
  499.             [ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  500.             NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
  501.             NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
  502.             [dirtPile addRegion:&eraseRect];
  503.     }    }
  504.  
  505.     // draw text messages
  506.     if (fruitPointCount) {
  507.         drawScore(ftx2, fty2, POINTS, fruitPoints);
  508.         ZAPRECT(textEraseRect, ftx, fty);
  509.     }
  510.     for (i=0; i<4; i++) flag[i] = NO;
  511.     for (i=0; i<4; i++) { // sort scores to draw in lowest to highest order
  512.     // A selection sort is used.  O(n^2) but n=4 so who cares?
  513.         int next = 0; int val = 100000;
  514.         for (j=0; j<4; j++) { // find next lowest, unused score value
  515.             if (!flag[j] && (ghostPoints[j] < val)) {
  516.                 val = ghostPoints[j]; next = j;
  517.             }
  518.         }
  519.         flag[next] = YES; order[i] = next;
  520.     }
  521.     for (i=0; i<4; i++) if (ghostPointCount[order[i]]) {
  522.         drawScore(gtx2[order[i]], gty2[order[i]], POINTS,
  523.                 ghostPoints[order[i]]);
  524.         ZAPRECT(textEraseRect, gtx[order[i]], gty[order[i]]);
  525.     }
  526.     if ((state == READY) || (state == DIEREADY)) {
  527.         [maze playerPosition:&x :&y];
  528.         drawReady(READY_X + 10 * scale, READY_Y + OFFSET_Y, POINTS);
  529.         ZAPRECT(readyRect, READY_X, READY_Y);
  530.     }
  531.     [buffer unlockFocus];
  532.     
  533.     // housekeeping for the graphics:
  534.     [self lockFocus];
  535.     [dirtPile doRedraw:buffer]; // flush buffer out
  536.     [self unlockFocus];
  537.     if (fruitOn == ERASE) fruitOn = NO;
  538.     NXPing();
  539.     erasePwr = NO;    // we've listened to this flag, so turn it off now.
  540.     return self;
  541. }
  542.  
  543. // Handle player movement.  We respond to the arrow keys.  If we don't
  544. // recognize the key, we pass it on.  This method just shunts the key
  545. // off to the player object, which is what really deals with it. 
  546. - keyDown:(NXEvent *)myevent
  547. {
  548.     unsigned short charCode;
  549.     unsigned short charSet;
  550.     register int i;
  551.     
  552.     if (!myevent) return self; // if no event when coalescing, go away
  553.     PSobscurecursor();
  554. #ifdef CHEATMODES
  555.     // secret cheat mode:  control-c
  556.     if (myevent->data.key.charCode == 0x03) {
  557.         cheatMode = YES;
  558.         fflush(stderr);
  559.         [preferences setUnfair]; // don't allow net high scores.  local OK
  560.         [customSound playNum:POWERDOTEATSOUND];
  561.         return self;
  562.     }
  563.     // secret cheat:  control-d (eats a power dot)
  564.     if (myevent->data.key.charCode == 0x04) {
  565.         for (i=0; i<4; i++) [ghost[i] powerDot:YES];
  566.         [preferences setUnfair]; // don't allow net high scores.  local OK
  567.         [customSound playNum:POWERDOTEATSOUND];
  568.         [scoreKeeper resetBonus:ghostId];
  569.         return self;
  570.     }
  571.     // secret cheat:  control-f (put fruit on screen)
  572.     if (myevent->data.key.charCode == 0x06) {
  573.         fruitCount = timeToFruit - 1;
  574.         numFruits = 0;
  575.         [preferences setUnfair]; // don't allow net high scores.  local OK
  576.         return self;
  577.     }
  578.     // secret cheat:  control-l (advance a level instantly)
  579.     if (myevent->data.key.charCode == 0x0c) {
  580.         NXRect tRect;
  581.         int x, y;
  582.         state = BLINK_LEVEL;
  583.         begin = 64;
  584.         fruitPointCount = 0; for (i=0; i<4; i++) ghostPointCount[i] = 0;
  585.         fruitOn = ERASE; // erase fruit if finished level
  586.         [maze playerPosition:&x :&y]; // get position of fruit
  587.         NXSetRect(&tRect, x * scale + mazePos.x,
  588.                 y * scale + mazePos.y, FRUIT_SIZE * scale,
  589.                 FRUIT_SIZE * scale);
  590.         [self rebuildStaticAt:&tRect];
  591.         fruitCount = 0;
  592.         [preferences setUnfair]; // don't allow net high scores.  local OK
  593.         return self;
  594.     }
  595. #endif
  596.     // check for arrow keys or space bar
  597.     if (!(myevent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK))) {
  598.         charCode = myevent->data.key.charCode;
  599.         charSet = myevent->data.key.charSet;
  600.         
  601.         if (charSet == NX_SYMBOLSET) { // symbol set contains the arrows
  602.             if          (charCode == 0xAD) { // Up Arrow
  603.                 [player newDirection:PAC_UP];
  604.                 if (paused) [controller unpause];
  605.                 return self;
  606.             } else if (charCode == 0xAF) { // Down Arrow
  607.                 [player newDirection:PAC_DOWN];
  608.                 if (paused) [controller unpause];
  609.                 return self;
  610.             } else if (charCode == 0xAC) { // Left Arrow
  611.                 [player newDirection:PAC_LEFT];
  612.                 if (paused) [controller unpause];
  613.                 return self;
  614.             } else if (charCode == 0xAE) { // Right Arrow
  615.                 [player newDirection:PAC_RIGHT];
  616.                 if (paused) [controller unpause];
  617.                 return self;
  618.             }
  619.         } else if (myevent->data.key.charCode == ' ') { // Space Bar
  620.             [player newDirection:PAC_STOP];
  621.             if (paused) [controller unpause];
  622.             return self;
  623.         } else if ((myevent->data.key.charCode == 'a') && cheatMode) {
  624. #ifdef CHEATMODES
  625.             // abort pac -- it's the only way you can die in cheat mode.
  626.             [customSound playNum:DEADSOUND];
  627.             state = DYING_PAC;
  628.             begin = 48; // stall
  629.             [player pacDie];
  630. #endif
  631.         } else {
  632.             [super keyDown:myevent];
  633.         }
  634.     } else [super keyDown:myevent];
  635.     [self keyDown:[NXApp peekAndGetNextEvent:NX_KEYDOWN]]; // coalesce keydowns
  636.     // this is done recursively...primitive, but easy to implement :-)
  637.     return self;
  638. }
  639.  
  640. // set up the screen at the start of a new level.  Loads in the maze.
  641. - setUpScreen
  642. {        
  643.     [maze makeMaze:screens[(([controller level] >= NUMSCREENS) ?
  644.         (NUMSCREENS - 1) : [controller level]) - 1]];
  645.     fruitOn = NO;
  646.     [self rebuildStaticBuffer];
  647.     [self startScreen];
  648.     timeToFruit = 200 + (random() & 0x01f0);
  649.     fruitCount = 0;
  650.     numFruits = 0;
  651.     [[scoreKeeper bonusTracker:fruitId] advanceBonus]; // changes with level
  652.     
  653.     [super setUpScreen];
  654.     return self;
  655. }
  656.  
  657. // This lets us put the ghosts back where they are at the start of the level.
  658. // This is separate from above because the above also resets the dots, and if
  659. // the player dies, we only want to reset the ghosts, not the dots, too.
  660. - startScreen
  661. {
  662.     const int *gh; int i;
  663.         
  664.     gh = [maze ghosts]; // pointer to array of ghost coordinates
  665.     for (i=0; i<=3; i++) {    // re-initialize each ghost
  666.         [ghost[i] initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
  667.         [ghost[i] setScale:scale];
  668.         ghostPointCount[i] = 0;
  669.     }
  670.     fruitPointCount = 0;
  671.     // don't let fruit come out too quickly
  672.     if (timeToFruit - fruitCount < 200)
  673.         timeToFruit += 250;
  674.     
  675.     return self;
  676. }
  677.  
  678. - restartGame
  679. {
  680.     return [self restartGameForDemo:NO];
  681. }
  682.  
  683. - restartGameForDemo:(BOOL)doingDemo
  684. {
  685.     // go to READY state to start game; but want DIEREADY so we don't advance
  686.     // the level; the controller, by virtue of calling this method, has already
  687.     // advanced the level.
  688.     state = DIEREADY;
  689.     begin = 100;
  690.     fruitOn = NO;
  691.     
  692.     // make sure that all artifacts of demo mode are gone
  693.     demoWait = 0;
  694.     cheatMode = NO;
  695.     if (demoMode) {
  696.         demoMode = NO;
  697.         [[self window] setTitle:"PacMan"];
  698.     }
  699.     
  700.     [scoreKeeper resetScore];    // clear the score
  701.     [scoreKeeper resetBonus:(-1)];    // clear all bonuses
  702.     [self getPreferences];    // make sure we're up to date
  703.     [player newPlayer];        // get a new pac to play with
  704.     // (above always sets up the pac, but in demoMode, the gameBrain hasn't
  705.     // given us 3 pacs, so we end up taking a negative # of pacs, which means
  706.     // demo mode will end as soon as the pac dies.)
  707.     if (doingDemo) demoMode = YES;
  708.     // cut off the info panel sound (if playing) (only if not demo mode)
  709.     else [[controller infoController] stopSound:self];
  710.     [customSound shutUpUntil:[[DAYTime alloc] initWithCurrentTime]];
  711.     [self update]; [self updateSelf:&bounds :1]; NXPing(); // show screen
  712.     // start up the sound player object with the "start game" music
  713.     if ([preferences music]) [customSound playNum:STARTLEVELSOUND];
  714.     return self;
  715. }
  716.  
  717. - setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember
  718. {
  719.     NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5},
  720.         {NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}};
  721.  
  722.     [super setBackgroundFile:fileName andRemember:remember];
  723.     [backGround2 lockFocus];
  724.     NXDrawGrayBezel(&bezel, &bounds);
  725.     NXFrameRectWithWidth(&bounds, 5);
  726.     NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6;
  727.     NX_X(&bezel) += 3; NX_Y(&bezel) += 3;
  728.     [self drawBackground:&bezel];
  729.     [backGround2 unlockFocus];
  730.     [self rebuildStaticBuffer];
  731.     [self update];
  732.     return self;
  733. }
  734.  
  735. - buildColorBackground
  736. {
  737.     NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5},
  738.         {NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}};
  739.  
  740.     [backGround2 lockFocus];
  741.     NXDrawGrayBezel(&bezel, &bounds);
  742.     NXFrameRectWithWidth(&bounds, 5);
  743.     NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6;
  744.     NX_X(&bezel) += 3; NX_Y(&bezel) += 3;
  745.     [self drawBackground:&bezel];
  746.     [backGround2 unlockFocus];
  747.     [self rebuildStaticBuffer];
  748.     return self;
  749. }
  750.  
  751. - acceptColor:(NXColor)color atPoint:(const NXPoint *)aPoint
  752. { // override to redraw background buffer.
  753.     backIsColor = YES;
  754.     backColor = color;
  755.     [self buildColorBackground];
  756.     [[self writeColor] update];
  757.     return self;
  758. }
  759.  
  760. - rebuildStaticBuffer
  761. {
  762.     NXRect mazeRect = {{0, 0},
  763.         {GHOST_SIZE * BLOCK_WIDTH, GHOST_SIZE * BLOCK_HEIGHT}};
  764.     int x, y;
  765.     register int f;
  766.     NXRect from;
  767.     NXPoint pos;
  768.         
  769.     [staticBuffer lockFocus];
  770.     [backGround2 composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)];
  771.     [maze render:&mazeRect at:&mazePos];
  772.     [maze playerPosition:&x :&y];
  773.     if (fruitOn) {
  774.         if (fruitOn != ERASE) {
  775.             // decide which fruit to draw
  776.             f = fruits[(([controller level] > FRUIT_LEVELS) ?
  777.                 FRUIT_LEVELS : [controller level])];
  778.             // draw the fruit
  779.             pos.x = x * scale + mazePos.x;
  780.             pos.y = y * scale + mazePos.y;
  781.             NXSetRect(&from,
  782.                 (f % FRUIT_PER_ROW) * FRUIT_SIZE * scale,
  783.                 (f / FRUIT_PER_ROW) * FRUIT_SIZE * scale,
  784.                 FRUIT_SIZE * scale, FRUIT_SIZE * scale);
  785.             [fruit[scale] composite:NX_SOVER fromRect:&from toPoint:&pos];
  786.             fruitOn = YES;
  787.         } else fruitOn = NO;
  788.     }
  789.     [staticBuffer unlockFocus];
  790.     
  791.     [buffer lockFocus];
  792.     [staticBuffer composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)];
  793.     [buffer unlockFocus];
  794.     [dirtPile addRegion:&bounds];
  795.     
  796.     return self;
  797. }
  798.  
  799. - rebuildStaticAt:(NXRect *)rect;
  800. {    // assumes that bezel is intact, so doesn't draw it
  801.     NXRect mazeRect = {{(NX_X(rect) - mazePos.x) / scale,
  802.             (NX_Y(rect) - mazePos.y) / scale},
  803.             {(NX_WIDTH(rect) - 1.0) / scale,
  804.             (NX_HEIGHT(rect) - 1.0) / scale}};
  805.     NXRect xRect = {{0.0, 0.0}, {GHOST_SIZE * scale, GHOST_SIZE * scale}};
  806.     NXPoint pos;
  807.     NXRect from;
  808.     register int f;
  809.     int x, y;
  810.  
  811.     [staticBuffer lockFocus];
  812.     [super rebuildStaticAt:rect];
  813.     [maze render:&mazeRect at:&mazePos];
  814.     // see if rect intersects fruit && fruit is on screen
  815.     [maze playerPosition:&x :&y];
  816.     NX_X(&xRect) = x * scale + mazePos.x;
  817.     NX_Y(&xRect) = y * scale + mazePos.y;
  818.     if (NXIntersectsRect(&xRect, rect) && fruitOn) {
  819.         if (fruitOn != ERASE) {
  820.             // decide which fruit to draw
  821.             f = fruits[(([controller level] > FRUIT_LEVELS) ?
  822.                 FRUIT_LEVELS : [controller level])];
  823.             // draw the fruit
  824.             pos.x = x * scale + mazePos.x;
  825.             pos.y = y * scale + mazePos.y;
  826.             NXSetRect(&from,
  827.                 (f % FRUIT_PER_ROW) * FRUIT_SIZE * scale,
  828.                 (f / FRUIT_PER_ROW) * FRUIT_SIZE * scale,
  829.                 FRUIT_SIZE * scale, FRUIT_SIZE * scale);
  830.             [fruit[scale] composite:NX_SOVER fromRect:&from
  831.                     toPoint:&(xRect.origin)];
  832.             fruitOn = YES;
  833.         } else fruitOn = NO;
  834.     }
  835.     [staticBuffer unlockFocus];
  836.     
  837.     [buffer lockFocus];
  838.     [staticBuffer composite:NX_COPY fromRect:rect toPoint:&(rect->origin)];
  839.     [buffer unlockFocus];
  840.     [dirtPile addRegion:rect];
  841.  
  842.     return self;
  843. }
  844.  
  845. - blinkPowerDot        // update static buffer where power dots are.
  846. {
  847.     const int *pd;    // pointer to array of power dot coords
  848.     register int i;
  849.     
  850.     [maze blinkPowerDot];
  851.     pd = [maze powerDot];    // get power dot coords
  852.     for (i=0; i<=3; i++) {    // erase power dot every time it blinks.
  853.             NX_X(&eraseRect) = pd[i * 2 ]    * GHOST_SIZE * scale + mazePos.x;
  854.             NX_Y(&eraseRect) = pd[i * 2 + 1] * GHOST_SIZE * scale + mazePos.y;
  855.             [self rebuildStaticAt:&eraseRect];
  856.     }
  857.     return self;
  858. }
  859.  
  860. - (int)scale
  861. {
  862.     return scale;
  863. }
  864.  
  865. - setScale:(int)newScale
  866. {
  867.     int i;
  868.  
  869.     if ((newScale < 1) || (newScale > 2)) return self;
  870.     if (newScale == scale) return self; // already that size.
  871.     
  872.     scale = newScale;
  873.     
  874.     for (i=0; i<4; i++) {
  875.         [ghost[i] setScale:scale];
  876.     }
  877.     [player setScale:scale];
  878.     [maze setScale:scale];
  879.     
  880.     // resize self and windows
  881.     [window disableFlushWindow];
  882.     [window disableDisplay];
  883.     [self sizeTo:windowX[scale] :windowY[scale]];
  884.     NXSetRect(&eraseRect, 0, 0, GHOST_SIZE * scale, GHOST_SIZE * scale);
  885.     NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale);
  886.     NXSetRect(&textEraseRect, 0, 0,
  887.             3 * GHOST_SIZE * scale, 2 * GHOST_SIZE * scale);
  888.  
  889.     
  890.     // rebuild all the offscreen buffers.
  891.     [buffer free];
  892.     [staticBuffer free];
  893.     [backGround2 free];
  894.     buffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size]; 
  895.     staticBuffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
  896.     backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
  897.      
  898.     if (backIsColor) [self buildColorBackground];
  899.     else [self setBackgroundFile:NXGetDefaultValue([NXApp appName],
  900.             "BackGround") andRemember:NO];
  901.  
  902.     [self rebuildStaticBuffer];
  903.     [[window delegate] windowDidMove:window];
  904.     [window sizeWindow:windowX[scale] :windowY[scale]];
  905.     [window reenableFlushWindow];
  906.     [window reenableDisplay];
  907.     [[window display] flushWindowIfNeeded];
  908.     [self update];
  909.     return self;
  910. }
  911.  
  912. @end
  913.